package com.speljohan.rsbot.gui;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;
import java.util.Map.Entry;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTree;
import javax.swing.ScrollPaneConstants;
import javax.swing.WindowConstants;
import javax.swing.JToggleButton.ToggleButtonModel;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.PlainDocument;
import javax.swing.text.StyleConstants;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;

import com.speljohan.rsbot.account.Account;
import com.speljohan.rsbot.script.Script;

/**
 * Original idea by Speljohan. Implemented by BAsh.
 */
public class ScriptSelector extends JDialog implements ActionListener {
	private final Logger log = Logger.getLogger(ScriptSelector.class.getName());

	private static Pattern badSubmit = Pattern.compile(
			"type\\w*=\\w*[\"\']*\\w*submit", Pattern.CASE_INSENSITIVE);
	private static Pattern badTags = Pattern
	.compile(
			"<(?:\\w+:)?(img|applet|frame|iframe|object|code|frameset|param|script|var)",
			Pattern.CASE_INSENSITIVE);

	private final String[] categories = { "Combat", "Magic", "Cooking",
			"Woodcutting", "Fletching", "Fishing", "Firemaking", "Crafting",
			"Smithing", "Mining", "Herblore", "Agility", "Thieving", "Slayer",
			"Farming", "Runecraft", "Hunter", "Construction", "Summoning",
			"Money", "Minigames", "Quests", "Development" };

	private static final String defaultDescription = "<html><head><title>Script</title></head><body>"
		+ "<h2>Script Information</h2><p>Please select a script.</p></body></html>";
	private static final long serialVersionUID = -4859515881079328266L;
	private JComboBox accountSelector;
	private JEditorPane infoPane;
	private boolean isOkay = false;

	private final Map<String, Script> scriptMap = new HashMap<String, Script>();

	private JTree scriptTree;

	public ScriptSelector(final Window parent,
			final Collection<Script> scripts, final Collection<Account> accounts) {
		super(parent, "Script Selector", ModalityType.APPLICATION_MODAL);
		for (final Script s : scripts) {
			final String cat = "/Scripts/"
				+ normaliseCategory(s.getScriptCategory()) + '/'
				+ s.getName();
			if (scriptMap.containsKey(cat)) {
				log.warning(s.getScriptCategory() + '/' + s.getName()
						+ " has the same name as another script.");
				scriptMap.put(cat, null);
			} else {
				scriptMap.put(cat, s);
			}
		}
		initComponents(accounts);
	}

	public void actionPerformed(final ActionEvent e) {
		final String command = e.getActionCommand().intern();
		if ("Cancel".equals(command)) {
			dispose();
		} else if ("OK".equals(command)) {
			isOkay = true;
			dispose();
		} else {
			log.warning("Action: " + command);
		}
	}

	private JTree buildScriptTree() {
		final SortedMap<String, DefaultMutableTreeNode> nodeMap = new TreeMap<String, DefaultMutableTreeNode>();
		nodeMap.put("/Scripts", new DefaultMutableTreeNode("Scripts"));
		for (final String cat : scriptMap.keySet()) {
			if (scriptMap.get(cat) == null) {
				continue;
			}
			int idx = 2;
			while ((idx = cat.indexOf('/', idx + 1)) != -1) {
				final String c = cat.substring(0, idx);
				if (!nodeMap.containsKey(c)) {
					nodeMap.put(c, null);
				}
			}
			if (!nodeMap.containsKey(cat)) {
				nodeMap.put(cat, null);
			}
		}
		for (final Entry<String, DefaultMutableTreeNode> ent : nodeMap
				.entrySet()) {
			final String key = ent.getKey();
			if (key.equals("/Scripts")) {
				continue;
			}
			final String name = key.substring(key.lastIndexOf('/'));
			String cat = key.substring(0, key.lastIndexOf('/'));
			if (cat.length() == 0) {
				cat += "/";
			}
			final DefaultMutableTreeNode node = new DefaultMutableTreeNode(name
					.substring(1));
			ent.setValue(node);
			nodeMap.get(cat).add(node);
		}
		final JTree tree = new JTree(nodeMap.get("/Scripts"));
		return tree;
	}

	public Account getAccount() {
		return (Account) accountSelector.getSelectedItem();
	}

	public Map<String, String> getArguments() {
		final HTMLDocument htmlDoc = (HTMLDocument) infoPane.getDocument();
		final Map<String, String> args = new HashMap<String, String>();
		for (final Element elem : htmlDoc.getRootElements()) {
			getArguments(args, elem);
		}
		return args;
	}

	private void getArguments(final Map<String, String> args, final Element elem) {
		final int len = elem.getElementCount();
		if (elem.getName().equalsIgnoreCase("input")
				|| elem.getName().equalsIgnoreCase("select")) {
			final AttributeSet as = elem.getAttributes();
			final Object model = as.getAttribute(StyleConstants.ModelAttribute);
			final String name = as.getAttribute(HTML.Attribute.NAME).toString();
			if (model instanceof PlainDocument) {
				final PlainDocument pd = (PlainDocument) model;
				String value = null;
				try {
					value = pd.getText(0, pd.getLength());
				} catch (final BadLocationException e) {
					e.printStackTrace();
				}
				args.put(name, value);
			} else if (model instanceof ToggleButtonModel) {
				final ToggleButtonModel buttonModel = (ToggleButtonModel) model;
				if (!args.containsKey(name)) {
					args.put(name, null);
				}
				if (buttonModel.isSelected()) {
					args.put(name, as.getAttribute(HTML.Attribute.VALUE)
							.toString());
				}
			} else if (model instanceof DefaultComboBoxModel) {
				args.put(name, ((DefaultComboBoxModel) model).getSelectedItem()
						.toString());
			} else {
				throw new Error("Unknown model [" + model.getClass().getName()
						+ "]");
			}
		}
		for (int i = 0; i < len; i++) {
			final Element e = elem.getElement(i);
			getArguments(args, e);
		}
	}

	public Script getScript() {
		final TreePath tp = scriptTree.getSelectionPath();
		if (tp == null) {
			return null;
		}
		final Object[] os = tp.getPath();
		final StringBuilder sb = new StringBuilder();
		for (final Object o : os) {
			final DefaultMutableTreeNode node = (DefaultMutableTreeNode) o;
			sb.append("/" + node.getUserObject());
		}
		return scriptMap.get(sb.toString());
	}

	private void initComponents(final Collection<Account> accounts) {
		final JSplitPane splitPaneTop = new JSplitPane();
		splitPaneTop.setBorder(null);
		scriptTree = buildScriptTree();
		infoPane = new JEditorPane();
		infoPane.setBorder(null);
		final JPanel optionsPane = new JPanel();
		optionsPane.setBorder(null);
		final BoxLayout bl = new BoxLayout(optionsPane, BoxLayout.X_AXIS);
		optionsPane.setLayout(bl);
		accountSelector = new JComboBox();

		final Container contentPane = getContentPane();
		contentPane.setLayout(new BorderLayout());
		// scriptTree.setPreferredSize(new Dimension(150,200));
		scriptTree.getSelectionModel().addTreeSelectionListener(
				new TreeSelectionListener() {
					public void valueChanged(final TreeSelectionEvent e) {
						final Script s = getScript();
						if (s != null) {
							setContent(s.getScriptDescription());
						} else {
							setContent(ScriptSelector.defaultDescription);
						}
					}
				});
		final JScrollPane treeScroll = new JScrollPane(scriptTree,
				ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
				ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		treeScroll.setBorder(null);
		treeScroll.setPreferredSize(new Dimension(250, 200));
		splitPaneTop.setLeftComponent(treeScroll);

		infoPane.setEditable(false);
		infoPane.setContentType("text/html");
		infoPane.addHyperlinkListener(new HyperlinkListener() {
			public void hyperlinkUpdate(final HyperlinkEvent e) {
				if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
					if (Desktop.isDesktopSupported()) {
						try {
							Desktop.getDesktop().browse(e.getURL().toURI());
						} catch (final IOException e1) {
							e1.printStackTrace();
						} catch (final URISyntaxException e1) {
							e1.printStackTrace();
						}
					}
				}
			}
		});
		final HTMLDocument htmlDoc = (HTMLDocument) infoPane.getDocument();
		htmlDoc.putProperty("imageCache", new Hashtable<URL, Image>());
		setContent(ScriptSelector.defaultDescription);
		infoPane.setPreferredSize(new Dimension(600, 370));
		splitPaneTop.setRightComponent(new JScrollPane(infoPane));

		optionsPane.add(Box.createHorizontalGlue());

		optionsPane.add(new JLabel("Account:"));
		accountSelector.setModel(new DefaultComboBoxModel(new Vector<Account>(
				accounts)));
		optionsPane.add(accountSelector);

		final JButton okayButton = new JButton("OK");
		okayButton.addActionListener(this);
		optionsPane.add(okayButton);

		final JButton cancelButton = new JButton("Cancel");
		cancelButton.addActionListener(this);
		optionsPane.add(cancelButton);

		setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
		contentPane.add(splitPaneTop, BorderLayout.CENTER);
		contentPane.add(optionsPane, BorderLayout.SOUTH);
		pack();
		setLocationRelativeTo(getOwner());
	}

	private String normaliseCategory(final String Name) {
		for (final String category : categories) {
			if (category.equalsIgnoreCase(Name)) {
				return category;
			}
		}
		return "Other";
	}

	private void setContent(String s) {
		if (ScriptSelector.badTags.matcher(s).find()
				|| ScriptSelector.badSubmit.matcher(s).find()) {
			s = "Error: Invalid Description";
		}
		// the following two lines fix a bug whereby
		// new HTML would assume previous CSS styles applied in the <head> tag
		infoPane.setContentType("text/plain");
		infoPane.setContentType("text/html");
		infoPane.setText(s);
	}

	/**
	 * True is the user hits okay, they have selected a script and an account.
	 */
	public boolean showToUser() {
		setVisible(true);
		return isOkay && getScript() != null & getAccount() != null;
	}
}
